using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
using System.Text;

namespace Crainiate.Data.Persistence.Cache
{
    public class ReflectionParser: ReflectionParserBase
    {
        //Working variables
        private List<PropertyInfo> _propertyList;
        private List<PropertyInfo> _valueList;
        private List<PropertyInfo> _referenceList;
        private List<PropertyInfo> _binaryList;

        #region Interface

        public override Type[] Types
        {
            get
            {
                return new Type[] { };
            }
        }

        public override void Parse(Type type)
        {
            ParseType(type);
        }
        
        public override void Validate(Type type)
        {
            ValidateType(type);
        }

        #endregion

        #region Implementation

        private void ParseType(Type type)
        {
            //Set the class level values
            PersistType = type;
            FullName = type.FullName;
            Name = type.Name;
            IsView = typeof(IView).IsAssignableFrom(type);
            BaseType = GetBaseType(type);

            if (IsView) PersistType = Crainiate.Data.Persistence.Providers.Common.GetPersistType(PersistType);

            //Get the key property values
            GetKeyProperties(PersistType);

            //Get the properties that arent primary or compound keys
            _propertyList = new List<PropertyInfo>();
            _valueList = new List<PropertyInfo>();
            _referenceList = new List<PropertyInfo>();
            _binaryList = new List<PropertyInfo>();

            if (IsView)
            {
                ConvertKeyProperties(type); //Make sure the view defines the primary or compound keys
                GetViewProperties(type);
            }
            else
            {
                GetProperties(type);
            }

            //Set the property arrays
            Properties = _propertyList.ToArray();
            ValueProperties = _valueList.ToArray();
            ReferenceProperties = _referenceList.ToArray();
            BinaryProperties = _binaryList.ToArray();

            _propertyList = null;
            _valueList = null;
            _referenceList = null;
            _binaryList = null;

            //Determine if this type is also the base type
            IsUnderlying = (type == UnderlyingType || IsView);

            //Get the rank
            Rank = GetRank(type);
        }

        private void ValidateType(Type type)
        {
            if (type.IsAbstract) throw new PersistenceException("Abstract type " + type.FullName + " cannot be persisted directly.");

            //Validate the class
            if (!IsView && !HasPrimaryKey && !HasCompoundKeys) throw new PersistenceException("Type " + type.FullName + " does not have a primary or compound key defined.");

            //Validate the compound key ranks
            if (CompoundKeys != null)
            {
                int lastRank = -1;
                foreach (PropertyInfo prop in CompoundKeys)
                {
                    object[] attributes = prop.GetCustomAttributes(false);
                    foreach (Attribute attr in attributes)
                    {
                        if (attr is ICompoundKeyColumnAttribute)
                        {
                            ICompoundKeyColumnAttribute key = (ICompoundKeyColumnAttribute)attr;

                            if (key.Rank == lastRank) throw new PersistenceException("Compound keys must have a unique rank.");
                            lastRank = key.Rank;
                        }
                    }
                }
            }

            if (HasObjects)
            {
                foreach (PropertyInfo prop in ReferenceProperties)
                {
                    if (prop.PropertyType.Equals(type)) throw new PersistenceException("Type " + type.FullName + " cannot have recursive reference properties.");     
                }
            }
        }

        //Loads primary or compound key information recursively
        private void GetKeyProperties(Type type)
        {
            //Get only the properties for this type.
            PropertyInfo[] propertyInfo = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.GetProperty);

            //Get pre recursive properties
            foreach (PropertyInfo prop in propertyInfo)
            {
                //Loop through attributes and get settings
                object[] attributes = prop.GetCustomAttributes(false);
                foreach (Attribute attr in attributes)
                {
                    if (attr is IColumnAttribute)
                    {
                        //Set to the base type for now, since at least one property is persisted
                        //it may be reset in a lower recursion
                        UnderlyingType = type;
                        UnderlyingName = type.Name;
                        break;
                    }
                }
            }

            //Load any base class properties if the base is not of type object
            if (!IsUnderlyingBase(type) && !IsBaseAbstract(type) ) GetKeyProperties(type.BaseType);

            SortedList<int, PropertyInfo> compoundList = new SortedList<int, PropertyInfo>();
            List<PropertyInfo> foreignList = new List<PropertyInfo>();

            //Get post recursive properties (attributes override those in lower classes)
            //All keys should be checked here
            foreach (PropertyInfo prop in propertyInfo)
            {
                //Loop through attributes and get settings
                object[] attributes = prop.GetCustomAttributes(false);
                foreach (Attribute attr in attributes)
                {
                    if (attr is IColumnAttribute)
                    {
                        //Add to PropertyInfo array if valid
                        if (!Component.Instance.IsAllowedType(prop.PropertyType)) throw new PersistenceException("Type " + prop.PropertyType.ToString() + " is not an allowed persistable type.");

                        if (attr is IPrimaryKeyColumnAttribute)
                        {
                            if (typeof(IPersistable).IsAssignableFrom(prop.PropertyType)) throw new PersistenceException("Object properties may not be declared as primary keys for Type " + type.FullName + ".");
                            if (HasPrimaryKey) throw new PersistenceException("A primary key has already been declared for Type " + type.FullName + ".");
                            if (HasCompoundKeys) throw new PersistenceException("A primary key may not be declared with a compound key for Type " + type.FullName + ".");
                            HasPrimaryKey = true;
                            PrimaryKey = prop;
                            KeyType = type;
                        }
                        else if (attr is IIdentityColumnAttribute)
                        {
                            //Check if an identity type 
                            IIdentityColumnAttribute key = (IIdentityColumnAttribute) attr;
                            if (key.Increment < 1) throw new PersistenceException("Identity Increment must be greater than zero.");
                            if (key.Start < 0) throw new PersistenceException("Identity start cannot be less than zero.");
                            IsIdentity = true;
                            Identity = prop;
                        }
                        else if (attr is ICompoundKeyColumnAttribute)
                        {
                            if (typeof(IPersistable).IsAssignableFrom(prop.PropertyType)) throw new PersistenceException("Object properties may not be declared as compound keys for Type " + type.FullName + ".");
                            if (HasPrimaryKey) throw new PersistenceException("A compound key may not be declared with a primary key for Type " + type.FullName + ".");
                            HasCompoundKeys = true;
                            KeyType = type;

                            ICompoundKeyColumnAttribute compound = (ICompoundKeyColumnAttribute)attr;
                            if (compoundList.ContainsKey(compound.Rank)) throw new PersistenceException("A compound key with rank " + compound.Rank.ToString() + " has already been declared.");
                            compoundList.Add(compound.Rank, prop);
                        }
                        else if (attr is IForeignKeyColumnAttribute)
                        {
                            if (typeof(IPersistable).IsAssignableFrom(prop.PropertyType)) throw new PersistenceException("Foreign key attributes may not be declared for object properties for Type " + type.FullName + " as they are already implicitly defined.");
                            HasForeignKeys = true;

                            foreignList.Add(prop);
                        }
                    }
                }
            }

            //Set the compound keys, the list must be sorted by rank
            if (compoundList.Count > 0)
            {
                CompoundKeys = ArrayFromList(compoundList);
            }

            //Set the foreign key list
            if (foreignList.Count > 0)
            {
                ForeignKeys = foreignList.ToArray();
            }
        }

        private void GetProperties(Type type)
        {
            PropertyInfo[] propertyInfo = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.GetProperty);

            //Get pre recursive properties
            foreach (PropertyInfo prop in propertyInfo)
            {
                //Loop through attributes and get settings
                object[] attributes = prop.GetCustomAttributes(false);

                bool isPrimary = false;
                bool isCompound = false;
                bool isColumn = false;

                foreach (Attribute attr in attributes)
                {
                    if (attr is IPrimaryKeyColumnAttribute) isPrimary = true;
                    if (attr is ICompoundKeyColumnAttribute) isCompound = true;
                    if (attr is IColumnAttribute) isColumn = true;
                }

                //Only use normal columns
                if (isColumn && !isPrimary && !isCompound)
                {
                    //Add to PropertyInfo array if valid
                    if (!Component.Instance.IsAllowedType(prop.PropertyType)) throw new PersistenceException("Type " + prop.PropertyType.ToString() + " is not an allowed persistable type for class " + type.ToString());

                    //Add to all properties                            
                    _propertyList.Add(prop);

                    //Determine if value or object property
                    if (typeof(IPersistable).IsAssignableFrom(prop.PropertyType))
                    {
                        HasObjects = true;

                        //Check that the referenced object has a primary key
                        ReflectionCacheItem refcache = Component.Instance.ReflectionCache.Lookup(prop.PropertyType);
                        if (!refcache.HasPrimaryKey) throw new PersistenceException("Property Type " + prop.PropertyType + " cannot reference a type that has no primary key.");

                        _referenceList.Add(prop);
                    }
                    else
                    {
                        _valueList.Add(prop);
                    }

                    if (typeof(Stream).IsAssignableFrom(prop.PropertyType))
                    {
                        HasBinaryProperties = true;
                        _binaryList.Add(prop);
                    }
                }
            }

            if (!IsUnderlyingBase(type) && type.BaseType.IsAbstract) GetProperties(type.BaseType);
        }

        //Convert the key properties to the view type from the persist type
        private void ConvertKeyProperties(Type type)
        {
            if (HasPrimaryKey)
            {
                string name = PrimaryKey.Name;
                PrimaryKey = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.GetProperty);
                if (PrimaryKey == null) throw new PersistenceException("The primary key field " + name + " was not found in the " + type.ToString() + " definition. This property is required.");

                KeyType = type;
            }
            else if (HasCompoundKeys)
            {
                List<PropertyInfo> list = new List<PropertyInfo>();

                foreach (PropertyInfo prop in CompoundKeys)
                {
                    string name = prop.Name;
                    PropertyInfo compound = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.GetProperty);

                    if (compound == null) throw new PersistenceException("The compound key field " + name + " was not found in the " + type.ToString() + " defintion. This property is required.");

                    list.Add(compound);
                }

                KeyType = type;
                CompoundKeys = list.ToArray();
            }
        }

        //Get view properties that match to properties in the persisted type
        private void GetViewProperties(Type type)
        {
            PropertyInfo[] propertyInfo = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.GetProperty);

            //Get all properties found in the persisted type
            foreach (PropertyInfo prop in propertyInfo)
            {
                if (PersistType.GetProperty(prop.Name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty) != null)
                {
                    _propertyList.Add(prop);

                    //Determine if value or object property
                    if (typeof(IPersistable).IsAssignableFrom(prop.PropertyType))
                    {
                        HasObjects = true;

                        _referenceList.Add(prop);
                    }
                    else
                    {
                        _valueList.Add(prop);
                    }

                    if (typeof(Stream).IsAssignableFrom(prop.PropertyType))
                    {
                        HasBinaryProperties = true;
                        _binaryList.Add(prop);
                    }
                }
            }
        }

        private PropertyInfo[] ArrayFromList(SortedList<int, PropertyInfo> list)
        {
            PropertyInfo[] array = new PropertyInfo[list.Count];
            int i = 0;

            foreach (PropertyInfo info in list.Values)
            {
                array[i] = info;
                i++;
            }

            return array;
        }

        #endregion
    }
}
